Realizar mapas con la mayor granularidad posible, a escala intraurbana, de vulnerabilidad social ante la pandemia de COVID-19. Se pone el foco en la situación de residentes migrantes, grupo para el cual los registros oficiales con frecuencia son escasos o inexistentes.
Con ello se busca contribuir en la toma de decisiones por parte de entidades municipales para la planificación y distribución de atención a nivel intraurbano (víveres con bancos de comida, atención médica in situ, etc.).
En esta instancia se utilizarán solo fuentes abiertas con cobertura global, de modo que el procedimiento sea reproducible en cualquier ciudad de Latinoamérica sin requerir fuentes locales específicas. Por supuesto, los datos locales de gran valor y podrán ser incorporados a éste procedimiento para refinar o extender los resultados.
Esta prueba de concepto toma al asentamiento “Villa Caracas” en Barranquilla, Colombia. Villa Caracas es un asentamiento donde se concentra población migrante. Originalmente un sector de terrenos ociosos, se ha convertido en sitio de edificación de viviendas precarias ante la falta de acceso a vivienda formal.
knitr::include_graphics("img/villa_caracas.png")
Predio del asentamiento Villa Caracas, 2016 (izq.) y 2019 (der.)
OpenStreetMap : datos de grilla vial pública, ubicación de hospitales y centros de salud, farmacias, tiendas y mercados donde comprar comida, etc.
Facebook Humanitarian Data : Densidad de población por rango etario - en particular +60 años
Portal de datos abiertos del Gobierno de Colombia : Límites adiministrativos municipales, datos en categorías de salud y protección social.
library(tidyverse) # funciones útiles en general para manipulación y visualización
library(sf) # para datos georeferenciados en formato vectorial
library(osmdata) # para acceso a datos de OpenStreetMap
library(leaflet) # para visualización de datos geográficos
library(nngeo) # para identificar elementos próximos en el espacio
# No disponible en CRAN, usar devtools::install_github("michaeldorman/nngeo")
Requerimos estimado georeferenciados de alta resolución espacial.
El portal oficial de datos, para Barranquilla, no ofrece resultados en la categoría “Estadísticas Nacionales”
Tampoco encontramos datos a nivel intra-municipal en el geoportal del Departamento Administrativo Nacional de Estadística (DANE) de Colombia.
Mientras se continúa buscando datos oficiales, podemos recurrir a los estimados demográficos para Colombia producidos por Facebook
url <- "https://data.humdata.org/dataset/2f865527-b7bf-466c-b620-c12b8d07a053/resource/357c91e0-c5fb-4ae2-ad9d-00805e5a075d/download/population_col_2018-10-01.csv.zip"
zipfile <- tempfile()
download.file(url, zipfile)
filename <- unzip(zipfile, list = TRUE)$Name
poblacion <- read_csv(unz(zipfile, filename))
unlink(zipfile)
El dataset contiene puntos definidos por pares de coordenadas en proyección Mercator, y estimados de población en torno a esa posición en 2015 y 2020, para toda Colombia:
head(poblacion)
Cada par latitud/longitud representa un área que corresponde a 1 segundo de arco de resolución (aproximadamente 30m x 30m)
Pueden descargarse del portal de datos abiertos de Colombia, como “https://www.datos.gov.co/Vivienda-Ciudad-y-Territorio/Limite-Distrito-de-Barranquilla/74fb-mmpu”
limites <- st_read("https://www.datos.gov.co/api/geospatial/74fb-mmpu?method=export&format=GeoJSON")
## Reading layer `OGRGeoJSON' from data source `https://www.datos.gov.co/api/geospatial/74fb-mmpu?method=export&format=GeoJSON' using driver `GeoJSON'
## Simple feature collection with 1 feature and 2 fields
## geometry type: MULTIPOLYGON
## dimension: XY
## bbox: xmin: -74.91979 ymin: 10.91388 xmax: -74.7549 ymax: 11.10681
## epsg (SRID): 4326
## proj4string: +proj=longlat +datum=WGS84 +no_defs
leaflet(limites) %>%
addProviderTiles(providers$CartoDB.Positron) %>%
addPolygons()
Debido a su naturaleza reciente e informal, no se espera encontrar un archivo georeferenciado con los límites de Villa Caracas. Como alternativa, se ha trazado y digitalizado su polígono en base a examen de imágenes aérea.
asentamiento <- st_read("data/villa_caracas.geojson")
## Reading layer `villa_caracas' from data source `/home/havb/Documents/repos/vulnerabilidad_COVID/notebook/poc/data/villa_caracas.geojson' using driver `GeoJSON'
## Simple feature collection with 1 feature and 1 field
## geometry type: MULTIPOLYGON
## dimension: XY
## bbox: xmin: -74.81359 ymin: 10.95573 xmax: -74.80969 ymax: 10.96053
## epsg (SRID): 4326
## proj4string: +proj=longlat +datum=WGS84 +no_defs
leaflet(limites) %>%
addProviderTiles(providers$CartoDB.Positron) %>%
addPolygons(fill = NA) %>%
addPolygons(data = asentamiento, color = "purple")
Nos interesa estimar accesibilidad de la población a efectores de servicios de salud, como hospitales, centros médicos y farmacias.
En el portal de datos oficial para Barranquilla no se hayan datos en la categoría “Salud y Protección Social”.
Es de esperarse que durante la gestión de la pandemia se identifiquen, o creen, listados oficiales de efectores de salud involucrados en la respuesta sanitarias. Hasta que éstos sean obtenidos, podemos recurrir a información disponible en OpenStreetMap, el repositorio global de información georeferenciada contribuida por voluntarios.
Para obtener datos georeferenciados locales realizaremos consultas a Overpass (http://overpass-api.de/), una interfaz que permite extraer información de la base de datos global de OpenStreetMap.
Overpass requiere que se especifique una “bounding box”, es decir las coordenadas de un rectángulo que abarque la zona de interés (en la práctica, los valores máximos y mínimos de latitud y longitud).
La generamos:
bbox <- matrix(st_bbox(limites), nrow = 2, dimnames = list(c("x", "y"), c("min", "max")))
bbox
## min max
## x -74.91979 -74.75490
## y 10.91388 11.10681
Para descargar entidades georeferenciadas se requiere conocer las palabras clave con las que se identifican los registros en la base de OSM. Existe gran detalle para el tipo de datos georeferenciados disponibles: áreas de parques públicos, posición de oficinas de correo o cajeros automáticos, vías de ferrocarril, etc. La nomenclatura se puede consultar en https://wiki.openstreetmap.org/wiki/Map_Features
En este caso vamos a solicitar todas las vías de circulación (calles, avenidas, autopistas, etc) de la ciudad. En la base de datos de OSM ciertas entidades agrupadas bajo la categoría “Healthcare” resultan de inmediato interés:
| key | value | description |
|---|---|---|
| amenity | clinic | A medium-sized medical facility or health centre. |
| amenity | hospital | A hospital providing in-patient medical treatment |
| amenity | pharmacy | Pharmacy: a shop where a pharmacist sells medications |
Para esta prueba de concepto descargaremos clínicas y hospitales.
clinicas <- opq(bbox) %>%
add_osm_feature(key = "amenity", value = "clinic") %>%
osmdata_sf()
hospitales <- opq(bbox) %>%
add_osm_feature(key = "amenity", value = "hospital") %>%
osmdata_sf()
Dependiendo del nivel de detalle con el que hayan sido registrados, la mayoría de las clínicas y hospitales están representados por puntos, pero en algunos casos por los polígonos de su planta. Para nuestros fines sólo necesitamos los puntos. Convertiremos los polígonos en puntos, tomando su centroide.
todo_a_puntos <- function(osm_query_sf) {
# extraemos los elementos de la lista que refieren a entidades OSM. Son dataframes con nombre
# "osm_points", "osm_lines", "osm_polygons", "osm_multilines", "osm_multipolygons"
puntos <- osm_query_sf[startsWith(names(osm_query_sf), "osm_")] %>%
# retiramos los que estan vacíos
compact() %>%
# nos quedamos solo con el campo "name"
map(~select(., name)) %>%
# combinamos en un solo dataframe
reduce(rbind) %>%
#tomamos los centroides
st_centroid()
# Solo retenemos los puntos con nombre, ya que los que tienen ese campo vacío
# parecen ser ubicaciones repetidas
filter(puntos, !is.na(name))
}
hospitales <- cbind(todo_a_puntos(hospitales), clase = "hospital")
clinicas <- cbind(todo_a_puntos(clinicas), clase = "clinica")
hospitales_y_clinicas <- rbind(clinicas, hospitales)
El resultado:
hospitales_y_clinicas
leaflet(hospitales_y_clinicas) %>%
addProviderTiles(providers$OpenStreetMap) %>%
addMarkers(popup = ~paste(clase, name)) %>%
addPolygons(data = limites, fill = NA)
Aquí es donde cada ciudad puede decidir su unidad geográfica de análisis. Lo ideal serían unidades estadísticas censales, con la mayor resolución (lo más pequeñas) que se disponga.
EL análisis a nivel de agregación similar al barrio (o distritos, comunas, etc) no es deseable debido a su baja resolución espacial. En ausencia de cartografía censal de alta granularidad, puede realizarse la partición de la superficie de la ciudad en celdas arbitrarias, lo suficientemente pequeñas.
Para este ejemplo particionaremos la superficie de la ciudad en celdas de unos 500m de radio (aproximando una superficie de 0.785 km2)
# Usamos una re-proyección equiareal para mayor precisión al calcular superficies
n_celdas <- limites %>%
st_transform(crs = "+proj=laea +ellps=WGS84 +units=m +no_defs") %>%
st_area() %>%
{. / (pi * 500^2)} %>%
as.numeric() %>%
round()
n_celdas
## [1] 199
Definiremos entonces 199 celdas.
celdas <- limites %>%
st_sample(size = 100000) %>% # sampleamos al azar un número grande, a gusto
st_coordinates() %>% # extraemos sendas columnas con long y lat
kmeans(centers = n_celdas) %>% # nuestros N grupos de puntos con separación máxima entre si
.$centers %>% # sólo queremos los centroides
as_tibble() %>% # convertimos en tibble que es lo que le gusta a sf
st_as_sf(coords = c("X", "Y"), crs = 4326) %>% # pasamos los centroide a objeto espacial
st_union() %>% # los combinamos en un sólo objeto multipunto
st_voronoi() %>% # particionamos en voronoi el espacio donde estan
st_collection_extract("POLYGON") %>% # del resultado extraemos los polígonos
st_intersection(limites) %>% # recortamos el cuadrado de los voronoi de acuerdo a los límites
st_sf %>% # Convertimos en dataframe espacial
mutate(id = rev(row_number())) # agregamos columna con id
ggplot(celdas) +
geom_sf() +
theme_void()
Para el ejemplo usaremos los estimados de población mayor a 60 años, como grupo de riesgo. También podría considerase tanto la población general como los mayores, combinando ambas variables un un índice agregado.
En aras de la performance, primero extraemos del dataset nacional los datos que caen dentro de la bounding box de la ciudad (esta operación es muy rápida). Habiendo limitado así los puntos a clasificar, toma mucho menos tiempo verificar a que celda de la ciudad corresponde cada punto del dataset.
poblacion_ciudad <- poblacion %>%
filter(between(latitude, bbox["y", "min"], bbox["y", "max"]),
between(longitude, bbox["x", "min"], bbox["x", "max"])) %>%
st_as_sf(coords = c("longitude", "latitude"), crs = 4326) %>%
st_join(celdas, left = FALSE) %>%
group_by(id)
ggplot(poblacion_ciudad) +
geom_sf(aes(color = population_2020), size = .0001, alpha = .3) +
scale_color_viridis_c() +
theme_minimal() +
labs(title = "Población por punto (estimado 2020)", color = "personas")
Agregamos la cantidad de personas por celda, en base a los punto que caen en cada una.
celdas <- poblacion_ciudad %>%
st_set_geometry(NULL) %>%
group_by(id) %>%
summarise(population_2020 = sum(population_2020, na.rm = TRUE)) %>%
{left_join(celdas, .)}
ggplot(celdas) +
geom_sf(aes(fill = population_2020), color = NA) +
scale_fill_viridis_c() +
theme_minimal() +
labs(title = "Población por área (estimado 2020)", fill = "personas")
Realizamos los mismos pasos para Villa Caracas
poblacion_asentamiento <- poblacion %>%
filter(between(latitude, bbox["y", "min"], bbox["y", "max"]),
between(longitude, bbox["x", "min"], bbox["x", "max"])) %>%
st_as_sf(coords = c("longitude", "latitude"), crs = 4326) %>%
st_join(asentamiento, left = FALSE)
asentamiento <- poblacion_asentamiento %>%
st_set_geometry(NULL) %>%
group_by(id) %>%
summarise(population_2020 = sum(population_2020, na.rm = TRUE)) %>%
{left_join(asentamiento, .)}
asentamiento$population_2020
## [1] 385.0497
Los datos utilizados resultan en un estimado de 385 personas residiendo de Villa Caracas.
Para estimar distancias entre cada celda y su efector de salud más cercano, crearemos una matriz Origen-Destino. Tomamos el centroide de cada celda, y determinamos el efector de salud (hospital o clínica) más cercano a su posición.
Como paso previo, descartamos celdas con población menor a un umbral dado:
umbral <- 1 # Solo celdas con al menos 1 persona
celdas <- celdas %>%
filter(population_2020 >= umbral)
Encontramos el elemento de la lista Y (sitios de salud) más cercano a cada elemento de la lista X (centroides de celdas).
En esta instancia tomaremos distancia lineal. En una futura iteración, estimaremos distancia a pie a través de la grilla de calles local usando OSRM
# Calculamos el centroide
centroides <- st_centroid(celdas)
# Identificamos el índice (la posición en el dataframe) de hospital/clínica más cercano a cada uno
id_cercanos <- unlist(st_nn(centroides, hospitales_y_clinicas, progress = FALSE))
# Calculamos la distancia entre cada par (por ahora lineal, a mejorar como distancia a pie por calles)
distancia_a_salud <- st_distance(centroides, hospitales_y_clinicas[id_cercanos,], by_element = TRUE)
# eliminamos la unidad (m)
distancia_a_salud <- as.numeric(distancia_a_salud)
# Agregamos as distancias
celdas <- cbind(celdas, distancia_a_salud)
Y lo mismo para Villa Caracas
# Calculamos el centroide
centroides <- st_centroid(asentamiento)
# Identificamos el índice (la posición en el dataframe) de hospital/clínica más cercano a cada uno
id_cercanos <- unlist(st_nn(centroides, hospitales_y_clinicas))
##
|
| | 0%
|
|======================================================================| 100%
# Calculamos la distancia entre cada par (por ahora lineal, a mejorar como distancia a pie por calles)
distancia_a_salud <- st_distance(centroides, hospitales_y_clinicas[id_cercanos,], by_element = TRUE)
# eliminamos la unidad (m)
distancia_a_salud <- as.numeric(distancia_a_salud)
# Agregamos as distancias
asentamiento <- cbind(asentamiento, distancia_a_salud)
ggplot(celdas) +
geom_sf(aes(fill = distancia_a_salud), color = NA) +
scale_fill_viridis_c(option = "plasma") +
theme_minimal() +
labs(title = "Distancia a efector de salud más cercano", fill = "m")
Combinando los datos demográficos con los de cercanía a salud, resaltamos las zonas donde se concentra población mayor, y a la vez se verifican las mayores distancias.
ggplot(celdas) +
geom_point(aes(x = population_2020, y = distancia_a_salud), alpha = .5) +
geom_point(data = asentamiento, aes(x = population_2020, y = distancia_a_salud), color = "purple") +
labs(x = "personas") +
theme_minimal()
En el gráfico podemos ver que el punto que corresponde al asentamiento Villa Caracas (en violeta), se encuentra relativamente próximo a un efector de salud.
En base al gráfico de dispersión, consideraremos casos crítico a las celdas con más de 500 residentes mayores que distan más de 1.5 km del centro de salud más cercano
celdas <- celdas %>%
mutate(sitio_critico = population_2020 > 500 & distancia_a_salud > 1500)
ggplot(celdas) +
geom_point(aes(x = population_2020, y = distancia_a_salud)) +
geom_point(data = filter(celdas, sitio_critico),
aes(x = population_2020, y = distancia_a_salud), color = "red", alpha = .5) +
labs(x = "personas") +
theme_minimal()
En el mapa:
celdas %>%
filter(sitio_critico) %>%
leaflet() %>%
addProviderTiles(providers$OpenStreetMap) %>%
addPolygons(color = "red", popup = ~paste("población:", round(population_2020),"</br>",
"distancia a salud:", round(distancia_a_salud), "m"))